<template>
  <div class="file-uploader-box">
    <el-card class="file-uploader-card">
      <!-- 功能菜单 -->
      <el-button-group class="flies-btn-list">
        <el-button type="primary" class="upload-btn-box">
          <el-upload
            class="upload-demo"
            action="#"
            :before-upload="beforeUpload"
            :on-change="filesOnChange"
            :auto-upload="false"
            :show-file-list="false"
          >
            <el-button type="primary" :icon="Files">选择文件</el-button>
          </el-upload>
        </el-button>

        <el-button type="primary" :icon="UploadFilled" @click="uploaderBtn">上传</el-button>
        <el-button type="success" :icon="FolderChecked" @click="mergeChunkBtn">合并切片</el-button>
        <el-button type="warning" :icon="VideoPause" @click="uploaderStopBtn">暂停</el-button>
        <el-button type="warning" :icon="VideoPlay" @click="uploaderRecoverBtn">恢复</el-button>
        <el-button type="danger" :icon="DeleteFilled" @click="uploaderClearBtn">清空</el-button>
      </el-button-group>

      <!-- 文件列表对象 -->
      <div class="uploader-files-list">
        <el-collapse v-model="activeNames" @change="handleChange">
          <el-collapse-item name="1" v-for="(item, index) in state.uploaderFiles" :key="index">
            <template #title>
              <div class="files-info">
                <el-row>
                  <el-col :span="5">文件名:{{ item.name }}</el-col>
                  <el-col :span="2">大小:{{ getFilsFormatSize(item.size) }}</el-col>
                  <el-col :span="5">
                    <div class="uploader-schedule">
                      <span>hash值计算:</span>
                      <el-progress class="uploader-jd-hash" :percentage="item.hashProgress" />
                    </div>
                  </el-col>
                  <el-col :span="8">
                    <div class="uploader-schedule">
                      <span>上传进度:</span>
                      <el-progress class="uploader-jd" :percentage="item.uploadProgress" />
                    </div>
                  </el-col>
                  <!-- <el-col :span="2">状态:{{ statusFormat(item.status) }}</el-col> -->
                </el-row>
              </div>
            </template>

            <!-- 切片table -->
            <el-table :data="item.chunkList" border style="width: 100%" height="400">
              <el-table-column prop="index" label="断点文件名" />
              <el-table-column prop="hash" label="切片MD5" />
              <el-table-column prop="size" label="切片大小" width="180">
                <template #default="scope">
                  <span>{{ getFilsFormatSize(scope.row.size) }}</span>
                </template>
              </el-table-column>
              <el-table-column prop="progress" label="上传进度" width="380">
                <template #default="scope">
                  <el-progress :percentage="scope.row.progress" />
                </template>
              </el-table-column>
              <el-table-column prop="status" label="是否完成">
                <template #default="scope">
                  <span>{{ scope.row.status }}</span>
                </template>
              </el-table-column>
            </el-table>
          </el-collapse-item>
        </el-collapse>
      </div>
    </el-card>
  </div>
</template>
<script setup lang="ts">
import { IFileStatusStr, IFileObj, IChunkReq, IChunkList, IChunkLists } from "../typings/interface";
import RequestDecorator from "../tools/RequestDecorator"
import axios from "axios";
import {
  Files,
  UploadFilled,
  VideoPause,
  VideoPlay,
  DeleteFilled,
  FolderChecked
} from "@element-plus/icons-vue";
import { reactive, toRaw, ref, onMounted } from "vue";
import { ElMessage } from 'element-plus'

// 上传状态数据
interface IState {
  worker: null | Worker;
  uploaderFiles: IFileObj[];
  uploaderFilesIndex: number;
  uploaderFilesErr: any[];
}

// file 对象
const chunkSize = 3 * 1024 * 1024; // 切片大小

// 单个文件的状态 对应描述
const fileStatusStr: IFileStatusStr = {
  wait: 'wait',
  uploading: 'uploading',
  success: 'success',
  error: 'error',
  secondPass: 'secondPass',
  pause: 'pause',
  resume: 'resume'
};

// 数据
const state: IState = reactive({
  worker: null,
  uploaderFiles: [],
  uploaderFilesIndex: 0,
  uploaderFilesErr: [] // 上传失败的promise
})

const activeNames = ref(['1']);// 折叠面板
let requestDecorator: any = null; // 并发sdk

/**
 * 上传前做的事
 * @param file 
 */
const beforeUpload = (file: File) => {
  return false
}

/**
 * @param file 
 */
const filesOnChange = (file: IFileObj) => {
  // 第一步：增加 子对象
  file.chunkList = []; // 切片对象
  file.hash = ""; // 默认hash
  file.uploadProgress = 0; // 上传进度条
  file.hashProgress = 0; // 计算hash 进度条
  file.fakeUploadProgress = 0; // 假进度条，处理恢复上传后，进度条后移的问题
  // state.uploaderFiles.push(file); // 如果是多个文件使用push
  state.uploaderFiles[0] = file;
}

// 总文件上传进度
const fileProgress = () => {
  const currentFile = state.uploaderFiles[state.uploaderFilesIndex]
  if (currentFile) {
    const uploadProgress = currentFile.chunkList.map((item: any) => item.size * item.progress).reduce((acc, cur) => acc + cur);
    const currentFileProgress = parseInt((uploadProgress / currentFile.size).toFixed(2));
    // 真假进度条处理--处理进度条后移
    if (!currentFile.fakeUploadProgress) {
      currentFile.uploadProgress = currentFileProgress;
    } else if (currentFileProgress > currentFile.fakeUploadProgress) {
      currentFile.uploadProgress = currentFileProgress;
    }
  }
}

/**
 * 请求
 * @param data 
 */
const handler = (data: any, index: number) => {
  return axios({
    method: "post",
    url: "http://192.168.31.61:8090/fileChunk",
    data: data,
    headers: { "Content-Type": "multipart/form-data" },
    onUploadProgress: (item: any) => {
      state.uploaderFiles[state.uploaderFilesIndex].chunkList[index].status = fileStatusStr.uploading
      state.uploaderFiles[state.uploaderFilesIndex].chunkList[index].progress = parseInt(String((item.loaded / item.total) * 100));
      fileProgress();
    }
  })
}


// 初始化
onMounted(() => {
  requestDecorator = new RequestDecorator({
    maxLimit: 5,
    requestApi: handler,
  });
})

/**
 * 创建切片
 * @param file 
 */
const createFileChunk = (file: IFileObj, size = chunkSize): Promise<IChunkList[]> => {
  return new Promise((res, rej) => {
    let count = 0; // 开始位置
    let filesChunkList: IChunkList[] = [];
    while (count < file.size) {
      filesChunkList.push(
        {
          file: file.raw.slice(count, count + size),
        }
      )
      count += size
    }
    res(filesChunkList)
  })
}

/**
 * 返回hash
 * @param fileChunkList 文件切片
 */
const calculateHash = (fileChunkList: IChunkList[]): Promise<string> => {
  return new Promise((res, rej) => {
    // const fileHashWorker = new URL('./hash.js', import.meta.url).href;
    state.worker = new Worker("./hash.js");
    state.worker.postMessage(toRaw(fileChunkList)); // 通信
    state.worker.onmessage = (e: any) => {
      const { percentage, hash } = e.data;
      if (state.uploaderFiles[state.uploaderFilesIndex]) {
        state.uploaderFiles[state.uploaderFilesIndex].hashProgress = Number(percentage.toFixed(0));
      }
      if (hash) {
        res(hash);
      }
    }
  })
}

/**
 * 
 * @param data 切片数据
 */
const readyFilesData = (fileChunkList: IChunkList[]) => {
  return new Promise((res, rej) => {
    state.uploaderFiles[state.uploaderFilesIndex].chunkList = fileChunkList.map((item: any, index: number) => {
      return {
        index,
        fileHash: state.uploaderFiles[state.uploaderFilesIndex].hash,
        fileName: state.uploaderFiles[state.uploaderFilesIndex].name,
        hash: state.uploaderFiles[state.uploaderFilesIndex].hash + '-' + index,
        chunk: item,
        size: item.file.size,
        uploaded: false, // 标识：是否已完成上传
        progress: 0,
        status: fileStatusStr.wait // 上传状态，用作进度状态显示
      }
    })
    res(true)
  })
}

/**
 * 上传
 * @param forms 
 */
const sendRequest = (chunkLists: IChunkLists[]) => {
  try {
    if (chunkLists.length <= 0) return;
    console.log("chunkLists -- > ", chunkLists);
    let promisesArr: any = []; // 所有的切片promise
    for (let i = 0; i < chunkLists.length; i++) {
      // 跳过已上传成功或已秒传的或失败的
      if (['secondPass', 'success', 'error'].includes(chunkLists[i].status)) {
        console.log('跳过已上传成功或已秒传的或失败的');
        continue;
      }
      let formsData = formDataFormat(chunkLists[i]);
      promisesArr.push(requestDecorator.request(formsData.formData, formsData.index).then((res: any) => {
        console.log("res -- >" + i, res);
        if (res.data.code === 200) {
          chunkLists[i].uploaded = true;
          chunkLists[i].status = fileStatusStr.success;
        } else {
          state.uploaderFilesErr.push(chunkLists[i]); // 存储上传失败的切片
          chunkLists[i].status = fileStatusStr.error;
        }
      }));

    }
    requestDecorator.concurrencyAll(promisesArr);
  } catch (err) {
    console.log("err", err);
    return false
  }
}

// 格式化form表单数据
const formDataFormat = (data: any) => {
  const formData = new FormData();
  formData.append('md5', data.fileHash);
  formData.append('file', data.chunk.file);
  formData.append('fileName', String(data.index)); // 文件名使用切片的下标
  return {
    formData,
    index: data.index,
    fileName: data.fileName
  };
}

/**
 * 上传切片
 * @param fileChunkList 切片数据
 */
const uploadChunks = (fileChunkList: IChunkLists[]) => {
  return new Promise(async (res, rej) => {
    try {
      let ret = await sendRequest(fileChunkList);
      res(true)
    } catch (err) {
      console.log("err", err);
      rej(false);
    }

  })
}

/**
 * 上传按钮
 */
const uploaderBtn = async () => {
  console.log("fileObj -- >", state.uploaderFiles[state.uploaderFilesIndex]);
  // 第一步：对文件进行切片
  let fileChunkList = await createFileChunk(state.uploaderFiles[state.uploaderFilesIndex]);
  console.log("fileChunkList --1 >", fileChunkList);

  // 第二步：计算hash
  state.uploaderFiles[state.uploaderFilesIndex].hash = await calculateHash(fileChunkList);
  await readyFilesData(fileChunkList);
  console.log("fileChunkLists --2 >", state.uploaderFiles[state.uploaderFilesIndex])

  // 第二步：上传切片
  await uploadChunks(state.uploaderFiles[state.uploaderFilesIndex].chunkList);

}

/**
 * 合并切片按钮
 */
const mergeChunkBtn = () => {
  axios({
    method: "post",
    url: "http://192.168.31.61:8090/fileChunkMerge",
    data: {
      md5: state.uploaderFiles[state.uploaderFilesIndex].hash,
      fileName: state.uploaderFiles[state.uploaderFilesIndex].name,
      fileChunkNum: state.uploaderFiles[state.uploaderFilesIndex].chunkList.length
    }
  }).then(res => {
    console.log("mergeChunk -- >", res);
    if (res.data.code === 2000) {
      ElMessage({
        message: res.data.message,
        type: 'success',
      })
    } else {
      ElMessage({
        message: "合并失败",
        type: "error",
      })
    }
  })
}

/**
 * 上传暂停按钮
 */
const uploaderStopBtn = () => {

}

/**
 * 恢复按钮
 */
const uploaderRecoverBtn = () => {

}

/**
 * 清除按钮
 */
const uploaderClearBtn = () => {
  state.uploaderFiles = [];
}

/**
 * 格式化文件大小
 * @param size 
 */
const getFilsFormatSize = (size: number): string => {
  if (!size) return "";
  let num = 1024;
  if (size < num) {
    return `${size}B`
  }
  if (size < Math.pow(num, 2))
    return (size / num).toFixed(2) + "KB"; //kb
  if (size < Math.pow(num, 3))
    return (size / Math.pow(num, 2)).toFixed(2) + "MB"; //M
  if (size < Math.pow(num, 4))
    return (size / Math.pow(num, 3)).toFixed(2) + "G"; //G
  return (size / Math.pow(num, 4)).toFixed(2) + "T"; //T
}

/**
 * 状态格式化
 * @param status 
 */
const statusFormat = (status: string) => {
  switch (status) {
    case "ready":
      return "待上传"
    default:
      return "-"
  }
}

/**
 * 折叠面板函数
 * @param val 
 */
const handleChange = (val: string[]) => {

}

</script>

<style lang="scss" scoped>
.file-uploader-box {
  padding: 20px;
}
.file-uploader-card {
  min-height: 600px;
  overflow-y: scroll;
}
.upload-btn-box {
  padding: 0 !important;
  border-right: none !important;
  border-right-color: none !important;
  border-radius: none !important;
}
.uploader-files-list {
  margin-top: 20px;
}
.files-info {
  width: 100%;
}
.uploader-schedule {
  width: 100%;
  display: flex;
}
.uploader-jd {
  width: calc(100% - 100px);
}
.uploader-jd-hash {
  width: 200px;
}
</style>